import libtcodpy as libtcod
import utils
import string
from random import randint, randrange
from utils import COLORS, CHARS, roll_dice
from math import sqrt
from copy import copy
from dialog import get_dialog, get_verb

#===============================================================================
# DEFAULT OBJECT
#===============================================================================

class Object(object):
    def __init__(self, map, x = 0, y = 0, char = ' ', fcolor = libtcod.white, bcolor = libtcod.black):
        self.map = map
        
        self.x = x
        self.y = y
        self.char = char
        self.fcolor = fcolor
        self.bcolor = bcolor
        self.transparent = True
        self.walkable = True
        self.explored = False
        self.name = 'Nothing'
        self.description = 'nothing' #You see 'description'
 
    def add_dialog(self, action, *params):
        self.map.game.hud.add_message(get_dialog(action, *params))

    def move(self, dx, dy):
        if utils.inside_map(self.x + dx, self.y + dy) and libtcod.map_is_walkable(self.map.tcodmap, self.x + dx, self.y + dy):
            self.x += dx
            self.y += dy
            return True
        else:
            return False
        
    def setpos(self, px, py):
        if utils.inside_map(px, py) and libtcod.map_is_walkable(self.map.tcodmap, px, py):
            self.x = px
            self.y = py
            return True
        else:
            return False
        
    def place_randomly(self, in_room = False):
        rx, ry = 0, 0
        if in_room:
            r = self.map.rooms[randint(0, len(self.map.rooms) - 1)]
            rx, ry = r[0] + r[2] / 2, r[1] + r[3] / 2
        else:
            t = randint(0, len(self.map.floor_tiles) - 1)
            rx, ry = self.map.floor_tiles[t].x, self.map.floor_tiles[t].y       
        self.setpos(rx, ry)
            
    def set_properties(self, transparent, walkable):
        self.transparent = transparent
        self.walkable = walkable
        
    def distance_to(self, x, y):
        return sqrt((self.x - x) ** 2 + (self.y - y) ** 2)
        
    def update(self):
        pass

    def draw(self, check_fov = False, explore = True, mx = utils.MAP_RECT[0], my = utils.MAP_RECT[1]):
        if check_fov:
            if self.map.in_fov(self.x, self.y) or self.map.lights_on:
                self.explored = True
                libtcod.console_put_char_ex(0, mx + self.x, my + self.y, self.char, self.fcolor, self.bcolor)
            elif self.explored and explore:
                libtcod.console_put_char_ex(0, mx + self.x, my + self.y, self.char, self.fcolor * utils.FOV_DARK, self.bcolor * utils.FOV_DARK)
        else:
            libtcod.console_put_char_ex(0, mx + self.x, my + self.y, self.char, self.fcolor, self.bcolor)

    def clear(self):
        libtcod.console_put_char(0, self.x, self.y, ' ', libtcod.BKGND_NONE)
        
#===============================================================================
# TILE
#===============================================================================
class Tile(Object):
    def __init__(self, map, x, y, char):
        Object.__init__(self, map, x, y, char)
        
    def draw(self):
        Object.draw(self, True)
        
#===============================================================================
# PLAYER
#===============================================================================

class Player(Object):
    #Init
    def __init__(self, map):
        Object.__init__(self, map)
        self.char = CHARS['player']
        self.fcolor = COLORS['player'][0]
        self.bcolor = COLORS['player'][1]
        self.name = map.game.player_data['name']
        self.description = 'yourself'
        
        self.inventory = self.map.game.inventory
        
        self.data = self.map.game.player_data
        self.steps = 0
        
        self.path = libtcod.path_new_using_map(self.map.tcodmap)
        self.path_show, self.path_walk, self.path_cursor = False, False, False
        self.path_cursor_x, self.path_cursor_y = None, None
        
        self.examine_cursor = False
        self.examine_cursor_x, self.examine_cursor_y = None, None

        self.place_randomly(True)
        
        self.last_x, self.last_y = self.x, self.y
        
        self.map.recompute_fov(self.x, self.y)
        self.update()
    
    #Move    
    def move(self, dx, dy):
        if self.path_cursor:
            self.map.update_objects = False
            if utils.inside_map(self.path_cursor_x + dx, self.path_cursor_y + dy):
                self.path_cursor_x += dx
                self.path_cursor_y += dy
        elif self.examine_cursor:
            self.map.update_objects = False
            if utils.inside_map(self.examine_cursor_x + dx, self.examine_cursor_y + dy):
                self.examine_cursor_x += dx
                self.examine_cursor_y += dy
                self.examine(self.examine_cursor_x, self.examine_cursor_y)
        else:
            dont_move = False
            for m in self.map.spawner.monsters:
                if self.x + dx == m.x and self.y + dy == m.y:
                    dont_move = True
                    self.map.update_objects = True
                    if m.state == 0:
                        m.battle_player()
                    break
            if not dont_move:
                Object.move(self, dx, dy)
    
    #Has Moved                
    def has_moved(self):
        return self.x != self.last_x or self.y != self.last_y
                    
    #Steps
    def step(self):
        self.steps += 1
        if self.steps % self.data['regen_rate'] == 0 and self.data['hp'] < self.data['max_hp']:
            self.data['hp'] += 1
    
    #Examine        
    def examine(self, x, y):
        if x == self.x and y == self.y:
            str = self.description
            params = ''
        else:
            str = 'nothing'
            params = ''
            #Tiles
            if self.map.tilemap[x][y].explored:
                str = self.map.tilemap[x][y].description
            
            #Stairs    
            if self.map.stairs_up.x == x and self.map.stairs_up.y == y:
                str = self.map.stairs_up.description
            if self.map.stairs_down.x == x and self.map.stairs_down.y == y:
                str = self.map.stairs_down.description
            
            #Items    
            for i in self.map.spawner.items:
                if x == i.x and y == i.y and i.explored:
                    str = i.description
                    break
            #Monsters
            for m in self.map.spawner.monsters:
                if x == m.x and y == m.y and self.map.in_fov(m.x, m.y):
                    if m.state == 0:
                        str = 'sleeping'
                    elif m.state == 1:
                        str = 'wandering'
                    else:
                        str = 'hunting'
                    params = m.name.lower()
                    self.map.game.hud.print_monster_stats(m)
                    break
        self.add_dialog('examine', str, params)
        
    #Pick up
    def pick_up(self):
        nothing = True
        for i in self.map.spawner.items:
            if self.x == i.x and self.y == i.y:
                str = ''
                for ii in self.data['inventory']:
                    str += ii.action_key
                if len(str) == len(utils.ACTION_KEYS):
                    self.add_dialog('full_inventory')
                    nothing = False
                    break
                for letter in utils.ACTION_KEYS:
                    if string.find(str, letter) == -1:
                        i.action_key = letter
                
                self.inventory.add_item(i)
                self.map.spawner.items.remove(i)
                self.add_dialog('pick_up', i.description)
                nothing = False
                break
        if nothing:
            self.add_dialog('pick_up_nothing')
    
    #Drop        
    def drop_item(self, item):
        self.inventory.remove_item(item)
        item.x, item.y = self.x, self.y
        self.map.spawner.items.append(item)
        self.add_dialog('drop', item.name)
    
    #Find Path    
    def find_path(self, px, py):
        if libtcod.map_is_in_fov(self.map.tcodmap, px, py) or self.map.tilemap[px][py].explored:
            libtcod.path_compute(self.path, self.x, self.y, px, py)
            self.path_cursor_x, self.path_cursor_y = px, py
    
    #Walk Path    
    def walk_path(self, px, py):
        self.find_path(px, py)
        self.path_walk = True
        self.path_show = True
        self.examine_cursor = False
        self.map.game.wait_for_keyboard = False
    
    #Stop Walking Path    
    def stop_path(self):
        self.path_walk = False
        self.path_show = False
        self.map.game.wait_for_keyboard = True
    
    #Show Path    
    def show_path(self, px, py):
        self.find_path(px, py)
        self.path_show = True
    
    #Show Cursor    
    def show_cursor(self, examine = False):
        if examine:
            self.examine_cursor = True
            self.path_cursor = False
            self.stop_path()
            self.examine_cursor_x = self.x
            self.examine_cursor_y = self.y
        else:
            self.path_cursor = True
            self.examine_cursor = False
            if self.path_cursor_x == None:
                self.path_cursor_x = self.x
                self.path_cursor_y = self.y
        
    #Hide Cursor
    def hide_cursor(self):
        self.path_cursor = False
        self.examine_cursor = False
    
    #Draw
    def draw(self):
        Object.draw(self)
        
        if self.path_cursor:
            libtcod.console_set_back(0, utils.MAP_RECT[0] + self.path_cursor_x, utils.MAP_RECT[1] + self.path_cursor_y, COLORS['path'], libtcod.BKGND_SET)
        elif self.examine_cursor:
            libtcod.console_set_back(0, utils.MAP_RECT[0] + self.examine_cursor_x, utils.MAP_RECT[1] + self.examine_cursor_y, COLORS['path'], libtcod.BKGND_SET)
        
        if self.path_show:
            for i in range(0, libtcod.path_size(self.path)):
                x, y = libtcod.path_get(self.path, i)
                libtcod.console_set_back(0, utils.MAP_RECT[0] + x, utils.MAP_RECT[1] + y, COLORS['path'], libtcod.BKGND_SET)
                
        self.last_x, self.last_y = self.x, self.y
        
    #Update
    def update(self):
        if self.path_walk:
            x, y = libtcod.path_walk(self.path, True)
            
            walk = True
            for m in self.map.spawner.monsters:
                if self.map.in_fov(m.x, m.y):
                    walk = False
                    break;
            
            if x != None and walk:
                self.x = x
                self.y = y
                self.step()
            else:
                self.stop_path()

        if self.has_moved():
            self.map.update_objects = True
            self.step()
            self.map.recompute_fov(self.x, self.y)
        
#===============================================================================
# STAIRS
#===============================================================================
class Stairs(Object):
    def __init__(self, map, up = True):
        Object.__init__(self, map)
        if up:
            self.char = CHARS['stairs_up']
        else:
            self.char = CHARS['stairs_down'] 
        self.name = 'Stairs'
        self.fcolor = COLORS['stairs'][0]
        self.bcolor = COLORS['stairs'][1]
        
        if up:
            self.x, self.y = map.player.x, map.player.y
            self.description = 'a winding staircase going to an upper level'
        else:
            self.description = 'a winding staircase going deeper into the darkness'
            furthest = [0, 0, 0]
            for r in self.map.rooms:
                rx, ry = r[0] + r[2] / 2, r[1] + r[3] / 2
                x, y = map.player.x, map.player.y
                dist = sqrt(pow(rx - x, 2) + pow(ry - y, 2))
                if dist > furthest[0]:
                    furthest = [dist, rx, ry]
            self.x, self.y = furthest[1], furthest[2]
        
    def draw(self):
        Object.draw(self, True)

#===============================================================================
# GENERIC MONSTER
#===============================================================================
class Monster(Object):
    def __init__(self, name = None):
        Object.__init__(self, None)
        self.name = name
        self.attack = 0
        self.max_hp = 0
        self.hp = 0
        self.sight = 0
        self.min_depth = 0
        self.bcolor = utils.COLORS['floor'][1]
        self.path = None
        self.state = 0
        self.player = None
        
    def die(self):
        self.map.spawner.monsters.remove(self)
        
    def battle_player(self):
        if self.hp > 0:
            #If the monster is sleeping
            if self.state == 0:
                player_damage = roll_dice(1, self.player.data['attack'])
                self.hp -= player_damage * 5
                if self.hp <= 0:
                    self.die()
                    self.add_dialog('kill_sleep', get_verb('kill_sleep'), self.name.lower())
                else:
                    self.state = 2
                    self.add_dialog('battle', get_verb('attack'), self.name.lower(), self.name.lower(), 'miss')
            else:
                #Determine damage given and received
                player_damage = roll_dice(1, self.player.data['attack'], -1)
                monster_damage = roll_dice(1, self.attack, -1) - roll_dice(1, self.player.data['defense'])
                
                self.player.data['hp'] -= utils.clamp(monster_damage, 0, monster_damage)
                self.hp -= utils.clamp(player_damage, 0, player_damage)
                
                if self.hp > 0 and self.player.data['hp'] > 0:
                    if player_damage == 0 and monster_damage == 0:
                        self.add_dialog('battle', 'miss', self.name.lower(), self.name.lower(), 'misses')
                    elif player_damage == 0 and monster_damage > 0:
                        self.add_dialog('battle', 'miss', self.name.lower(), self.name.lower(), get_verb('attack', True))
                    elif player_damage > 0 and monster_damage == 0:
                        self.add_dialog('battle', get_verb('attack'), self.name.lower(), self.name.lower(), 'misses')
                    else:
                        self.add_dialog('battle', get_verb('attack'), self.name.lower(), self.name.lower(), get_verb('attack', True))
                    
                if self.hp <= 0:
                    self.die()
                    self.add_dialog('kill', self.name.lower(), get_verb('kill'))
                    
                #Player Died
                if self.player.data['hp'] <= 0:
                    self.add_dialog('death')
                    self.map.game.death = True
                    self.map.game.stop = True
        
    def draw(self):
        Object.draw(self, True, False)
        
    def update(self):
        #Sleep
        if self.state == 0:
            if roll_dice(1, 15) == 1:
                self.state = 1
        #Wander
        elif self.state == 1:
            if roll_dice(1, 200) == 1:
                self.state = 0
            elif self.distance_to(self.map.player.x, self.map.player.y) <= self.sight:
                self.state = 2
            else:
                if libtcod.path_is_empty(self.path):
                    t = self.map.floor_tiles[randrange(len(self.map.floor_tiles))]
                    libtcod.path_compute(self.path, self.x, self.y, t.x, t.y)
                    x, y = libtcod.path_walk(self.path, True)
                else:
                    x, y = libtcod.path_walk(self.path, True)
                if x != None or y != None:
                    self.x, self.y = x, y
        #Hunt
        else:
            if self.distance_to(self.map.player.x, self.map.player.y) > self.sight:
                self.state = 1
            else:
                libtcod.path_compute(self.path, self.x, self.y, self.map.player.x, self.map.player.y)
                x, y = libtcod.path_walk(self.path, True)
                if x != None or y != None:
                    if x == self.map.player.x and y == self.map.player.y:
                        #Battle the player
                        self.battle_player()
                    else:
                        walk = True
                        for m in self.map.spawner.monsters:
                            if m != self:
                                if m.x == x and m.y == y:
                                    walk = False
                        if walk:
                            self.x, self.y = x, y    
    
    def __repr__(self):
        return '<Monster "%s"(%s) @ (%d, %d)>' % (self.name, self.char, self.x, self.y)
    
#===============================================================================
# GENERIC ITEM
#===============================================================================
class Item(Object):
    def __init__(self, name = None):
        Object.__init__(self, None)
        self.name = name
        self.type = "unknown"
        self.requirement = 0
        self.equipped = False
        self.min_depth = 0
        self.attack = 0
        self.defense = 0
        self.action_key = None
        self.fcolor = utils.COLORS['items'][0]
        self.bcolor = utils.COLORS['items'][1]
        
    def draw(self):
        Object.draw(self, True)
    
    def __repr__(self):
        return '<%s "%s"(%d,%d,%d) @ (%d, %d)>' % (self.type.capitalize(), self.name, self.requirement, self.attack, self.defense, self.x, self.y)
    
#===============================================================================
# OBJECT SPAWNER
#===============================================================================
class Spawner(object):
    def __init__(self, map, monster_list, item_list):
        self.map = map
        self.depth = map.game.current_depth

        self.possible_monsters = []
        self.possible_items = []
        for m in monster_list:
            if m.min_depth <= self.depth + 1:
                self.possible_monsters.append(m)
        for i in item_list:
            if i.min_depth <= self.depth + 1:
                self.possible_items.append(i)
        
        self.items = []
        self.monsters = []
        self.max_attemps = randint(utils.clamp(self.depth, utils.MIN_OBJECTS, self.depth), (self.depth + 1) * 3)
        for _ in range(self.max_attemps):
            #Spawn Monsters
            if roll_dice(1, 10) > 3 or len(self.monsters) + len(self.items) < utils.MIN_OBJECTS:
                m = copy(self.possible_monsters[roll_dice(1, len(self.possible_monsters)) - 1])
                m.map = self.map
                m.path = libtcod.path_new_using_map(self.map.tcodmap)
                m.state = roll_dice(1, 2, -1)
                m.player = self.map.player
                while (True):
                    m.place_randomly()
                    player, stairs_up, stairs_down, monsters, items = True, True, True, True, True
                    if m.x == self.map.player.x and m.y == self.map.player.y:
                        player = False
                    if m.x == self.map.stairs_up.x and m.y == self.map.stairs_up.y:
                        stairs_up = False
                    if m.x == self.map.stairs_down.x and m.y == self.map.stairs_down.y:
                        stairs_down = False
                    for mm in self.monsters:
                        if m.x == mm.x and m.y == mm.y:
                            monsters = False
                            break
                    for ii in self.items:
                        if m.x == ii.x and m.y == ii.y:
                            items = False
                            break
                    if player and stairs_up and stairs_down and monsters and items:
                        break
                self.monsters.append(m)
            #Spawn Items
            if roll_dice(1, 10) > 7 or len(self.monsters) + len(self.items) < utils.MIN_OBJECTS:
                i = copy(self.possible_items[roll_dice(1, len(self.possible_items)) - 1])
                i.map = self.map
                while (True):
                    i.place_randomly()
                    player, stairs_up, stairs_down, monsters, items = True, True, True, True, True
                    if i.x == self.map.player.x and i.y == self.map.player.y:
                        player = False
                    if i.x == self.map.stairs_up.x and i.y == self.map.stairs_up.y:
                        stairs_up = False
                    if i.x == self.map.stairs_down.x and i.y == self.map.stairs_down.y:
                        stairs_down = False
                    for mm in self.monsters:
                        if i.x == mm.x and i.y == mm.y:
                            monsters = False
                            break
                    for ii in self.items:
                        if i.x == ii.x and i.y == ii.y:
                            items = False
                            break
                    if player and stairs_up and stairs_down and monsters and items:
                        break
                self.items.append(i)
        
    def draw(self):
        for i in self.items:
            i.draw()
        for m in self.monsters:
            m.draw()
            
    def update(self):
        for i in self.items:
            i.update()
        for m in self.monsters:
            m.update()
            
    def __repr__(self):
        list = []
        for i in self.item_list:
            list.append(i)
        for m in self.monster_list:
            list.append(m)
        return list
    
#===============================================================================
# READ MONSTER FILE
#===============================================================================
def read_monsters(file):
    parser = libtcod.parser_new()
    monster_type = libtcod.parser_new_struct(parser, 'monster')
    libtcod.struct_add_property(monster_type, 'char', libtcod.TYPE_CHAR, True)
    libtcod.struct_add_property(monster_type, 'fcolor', libtcod.TYPE_COLOR, True)
    libtcod.struct_add_property(monster_type, 'hp', libtcod.TYPE_INT, True)
    libtcod.struct_add_property(monster_type, 'attack', libtcod.TYPE_INT, True)
    libtcod.struct_add_property(monster_type, 'sight', libtcod.TYPE_INT, True)
    libtcod.struct_add_property(monster_type, 'min_depth', libtcod.TYPE_INT, True)
    libtcod.struct_add_property(monster_type, 'description', libtcod.TYPE_STRING, True)
    
    monsters = []
    class MonsterListener:
        def new_struct(self, struct, name):
            if libtcod.struct_get_name(struct) != 'monster':
                return False
            self.monster = Monster(name)
            return True
        def new_flag(self, name):
            return False
        def new_property(self, name, typ, value):
            if name == 'char':
                self.monster.char = value
            elif name == 'fcolor':
                self.monster.fcolor = value
            elif name == 'hp':
                self.monster.hp = value
                self.monster.max_hp = value
            elif name == 'attack':
                self.monster.attack = value
            elif name == 'sight':
                self.monster.sight = value
            elif name == 'min_depth':
                self.monster.min_depth = value
            elif name == 'description':
                self.monster.description = value
            else:
                return False
            return True
        def end_struct(self, struct, name):
            monsters.append(self.monster)
            return True
        def error(self, msg):
            print msg
            utils.write_error(msg)
            return True
        
    libtcod.parser_run(parser, file, MonsterListener())
    libtcod.parser_delete(parser)
    
    return monsters

#===============================================================================
# READ ITEMS FILE
#===============================================================================
def read_items(file):
    parser = libtcod.parser_new()
    
    item_type = libtcod.parser_new_struct(parser, 'item')
    libtcod.struct_add_property(item_type, 'type', libtcod.TYPE_STRING, True)
    libtcod.struct_add_property(item_type, 'requirement', libtcod.TYPE_INT, True)
    libtcod.struct_add_property(item_type, 'attack', libtcod.TYPE_INT, True)
    libtcod.struct_add_property(item_type, 'defense', libtcod.TYPE_INT, True)
    libtcod.struct_add_property(item_type, 'min_depth', libtcod.TYPE_INT, True)
    libtcod.struct_add_property(item_type, 'description', libtcod.TYPE_STRING, True)
    
    items = []
    
    class ItemListener:
        def new_struct(self, struct, name):
            self.item = Item(name)
            return True
        def new_flag(self, name):
            return False
        def new_property(self, name, typ, value):
            if name == 'type':
                self.item.type = value
                self.item.char = CHARS.get(value, '?')
            elif name == 'requirement':
                self.item.requirement = value
            elif name == 'description':
                self.item.description = value
            elif name == 'attack':
                self.item.attack = value
            elif name == 'defense':
                self.item.defense = value
            elif name == 'min_depth':
                self.item.min_depth = value
            else:
                return False
            return True
        def end_struct(self, struct, name):
            items.append(self.item)
            return True
        def error(self, msg):
            print msg
            utils.write_error(msg)
            return True
        
    libtcod.parser_run(parser, file, ItemListener())
    libtcod.parser_delete(parser)
    
    return items

if __name__ == "__main__":
    print "Please run main.py!"
